package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisCacheUtils;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    private static final Long CACHE_NULL_TTL = 60L;
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    ExecutorService executorService = Executors.newFixedThreadPool(3);

    @Resource
    RedisCacheUtils redisCacheUtils;

    @Override
    public Result queryShopById(Long id) {

//        Shop shop = redisCacheUtils.queryByPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, TimeUnit.MINUTES, CACHE_NULL_TTL);
//        return Result.ok(shop);

//        return queryShopByMutex(id);

        return  queryShopByLogicExpire(id);
    }




    /**
     * 逻辑到期的情况解决缓存击穿
     */
    private Result queryShopByLogicExpire(Long id) {
        if (id == null) {
            return Result.fail("店铺id不能为null");
        }

        //1.先从redis里查,看是否命中。如果未命中，则说明不存在,直接返回null.不为null，则判断是否过期，如
        //果未过期，直接返回
        //逻辑过期这种方案一般针对优惠券等只在固定期限的数据，一般会提前最好缓存预热.所以如果不存在，即说明该
        //key已经过期被手动删除或不在活动期之类的.直接返回null.因此这里也不需考虑缓存穿透的情况。
        String shopKey = CACHE_SHOP_KEY + id;
        String s = stringRedisTemplate.opsForValue().get(shopKey);
        if (StrUtil.isBlank(s)) {
            return Result.fail("redis不存在该店铺信息");
        }
        //2.已到期
        RedisData redisData = JSONUtil.toBean(s, RedisData.class);
        //redisData里的Shop信息，因为无法制定Shop.class，所以其实只能被反序列化为JSONOjcect对象，需要自己手动转成Shop对象
        JSONObject data = (JSONObject)redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        if (redisData.getExpireTime().isBefore(LocalDateTime.now())) {
            log.info("数据已到期...");
            //2.1 尝试获取锁
            Boolean lock = tryLock(RedisConstants.LOCK_SHOP_KEY+id);
            //2.1.1 获取锁成功，异步开启一个现场去更新数据(注意释放锁)，然后返回旧的数据.
            if (lock) {
                executorService.submit(()->{
                    try {
                        saveShop2Redis(id, 10L);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        unlock(RedisConstants.LOCK_SHOP_KEY+id);
                    }
                });
                return Result.ok(shop);
            }
            //2.1.2 未获取锁成功，直接返回旧的数据
            return Result.ok(shop);
        }

        //3.如果未到期，直接返回
        return Result.ok(shop);
    }

    /**
     * 互斥锁来解决缓存击穿的方案
     * 思路：即通过争夺存在redis的锁来获得数据库的查询权利。在查询完成后，
     * 释放锁。 不过这个没有考虑服务宕机锁一直得不到解锁的情况，应该来说算不
     * 上真正完整的分布式锁。 但可以保证服务正常的情况下，key失效后，大量的
     * 请求只有一个进入数据库查询。
     */
    private Result queryShopByMutex(Long id) {
        if (id == null) {
            return Result.fail("店铺id不能为null");
        }
        //1.先从redis里查,查到了直接返回
        String shopKey = CACHE_SHOP_KEY + id;
        String s = stringRedisTemplate.opsForValue().get(shopKey);
        if (StrUtil.isNotBlank(s)) {
            Shop shop = JSONUtil.toBean(s, Shop.class);
            return Result.ok(shop);
        }
        if (s != null){ //s.isNotBlank且s！=null.即s=""
            return Result.fail("店铺不存在");
        }

        //2.redis中没有，从数据库查.同时将查询出的数据缓存到redis里
        //2.1 针对缓存击穿的问题。只允许一个线程去查询数据库，先加锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY+ id;
        Shop shop = null;
        try {
            if (!tryLock(lockKey)) {
                //2.2 获取锁失败，则睡眠一段时间，再次尝试
                log.info("获取锁失败...");
                Thread.sleep(50);
                return queryShopById(id);
            }

            //2.3 获取锁成功，则查询数据库
            //这里即是获得锁，也需要进行再次验证缓存是否信息.因为可能是另一个线程释放锁的瞬间，获得了锁.但此时redis缓存已经被存入呢
            if (stringRedisTemplate.opsForValue().get(shopKey)!=null) {  //这里及时获得锁，也需要进行再次验证缓存是否信息
                return queryShopById(id);
            }
            Thread.sleep(200);  //模拟查询需要花费较长的时间,用来测试加锁是否能解决缓存穿透
            shop = getById(id);

            if (shop == null){
                //缓存穿透的处理,即使店铺不存在，也将空值存入redis.注意设置过期时间
                stringRedisTemplate.opsForValue().set(shopKey,"",
                        RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return Result.fail("店铺不存在");
            }
            String jsonStr = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(shopKey, jsonStr);
            return Result.ok(shop);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            unlock(lockKey);
        }

        return Result.fail("执行异常");
    }

    void test(Long id ,Function<Long,Object> function){
        function.apply(id);
    }

    public void testFunction(int a){
        test(1L, new Function<Long, Object>() {
                    @Override
                    public Object apply(Long id) {
                        //匿名内部类，是内部类，所以可以调到该类的函数.而该类对象传入函数自然也可以调用到
                        return getById(id);
                    }
                }
        );

        test(1L, id->getById(id)
        );

        test(1L, this::getById
        );
    }

    public void saveShop2Redis(Long id, Long expireTime){
        Shop shop = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, JSONUtil.toJsonStr(redisData));
    }


    private boolean tryLock(String key) {
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(key, "", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        if (BooleanUtil.isTrue(isLock)){ //这里视频里担心isLock为null，导致拆箱出现问题.仔细想想返回值确实可能有返回null的可能
            log.info("获取锁成功");
            return isLock;
        }
        return false;
    }

    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

    @Override
    @Transactional
    public void updateShopById(Shop shop) {
        /**
         * 保证redis、数据库的数据一致性.两者需要一起成功/失败
         */
        //1.先更新数据库
        updateById(shop);

        //2.再删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY+shop.getId());
    }
}
